package com.atguigu.gulimall.cart.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.constant.CartConstant;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.cart.feign.ProductFeignService;
import com.atguigu.gulimall.cart.intercept.CartIntercept;
import com.atguigu.gulimall.cart.service.CartService;
import com.atguigu.gulimall.cart.vo.Cart;
import com.atguigu.gulimall.cart.vo.CartItem;
import com.atguigu.gulimall.cart.vo.SkuInfoVo;
import com.atguigu.gulimall.cart.vo.UserInfo;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

/**
 * @author gcq
 * @Create 2021-01-03
 */
@Service
public class CartServiceImpl implements CartService {

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    ThreadPoolExecutor executor;

    @Autowired
    ProductFeignService productFeignService;

    private final String CART_PREFIX = "gulimall:cart:";

    /**
     * 给购物车里面添加商品
     * @param skuId 商品id
     * @param num 数量
     * @throws ExecutionException
     * @throws InterruptedException
     * 如果远程查询比较慢，比如方法当中有好几个远程查询，都要好几秒以上，等整个方法返回就需要很久，这块你是怎么处理的?
     *  1、为了提交远程查询的效率，可以使用线程池的方式，异步进行请求
     *  2、要做的操作就是将所有的线程全部放到自己手写的线程池里面
     *  3、每一个服务都需要配置一个自己的线程池
     *  4、完全使用线程池来控制所有的请求
     */
    @Override
    public void addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
        // 获取到要操作的购物车
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        // 1、如果购物车中已有该商品那么需要做的就是修改商品的数量，如果没有该商品需要进行添加该商品到购物车中
        String result = (String) cartOps.get(skuId.toString()); // 商品id作为键，先去Redis里面查看有没有这个相同的商品
        if (StringUtils.isEmpty(result)) { // 如果返回为空，那么说明购物车中没有这个商品，那就要执行添加
            // 2、添加商品到购物车
            // 第一个异步任务,查询当前商品信息
            CartItem cartItem = new CartItem(); // 购物车中每一个都是一个购物项，封装购物车的内容
            CompletableFuture<Void> completableFutureSkuInfo = CompletableFuture.runAsync(() -> {
                // 2.1、远程查询出当前要添加的商品信息
                // 添加哪个商品到购物车，先查询到这个商品的信息
                R r = productFeignService.getSkuInfo(skuId);
                // 获取远程返回的数据，远程数据都封装在skuInfo中
                SkuInfoVo skuInfo = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                });
                cartItem.setCheck(true); // 添加商品到购物车，那么这个商品一定是选中的
                cartItem.setSkuId(skuInfo.getSkuId()); // 查询的是哪个商品的id，那么这个商品id就是哪个
                cartItem.setImage(skuInfo.getSkuDefaultImg()); // 当前商品的图片信息
                cartItem.setPrice(skuInfo.getPrice()); // 当前商品的价格
                cartItem.setTitle(skuInfo.getSkuTitle()); // 当前商品的标题
                cartItem.setCount(num); // 当前商品添加的数量
            }, executor);
            // 3、第二个异步任务，远程查询sku组合信息
            CompletableFuture<Void> completableFutureSkuAttr = CompletableFuture.runAsync(() -> {
                List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId); // 根据skuid来查
                cartItem.setSkuAttr(skuSaleAttrValues); // 远程查询出来到sku组合信息，需要在购物车中进行显示
            }, executor);
            // 4、两个异步任务都完成，才能把数据放到redis中
            CompletableFuture.allOf(completableFutureSkuInfo,completableFutureSkuAttr).get();
            // 把购物项的数据保存redis中
            String cartItemJson = JSON.toJSONString(cartItem);
            cartOps.put(skuId.toString(),cartItemJson); // 添加商品到购物车中
        } else {
            // 如果购物车中有这个商品，那么需要做的就是将商品的数量进行更改,也即是新添加的商品数量加上当前购物车中商品数量
            CartItem cartItem = JSON.parseObject(result, CartItem.class);
            cartItem.setCount(cartItem.getCount() + num);
            String cartItemJson = JSON.toJSONString(cartItem);
            // 更新redis
            cartOps.put(skuId.toString(),cartItemJson);
        }
    }

    @Override
    public CartItem getCartItem(Long skuId) {
        // 1、绑定hash操作
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        // 2、从redis中拿到值
        String res = (String) cartOps.get(skuId.toString());
        // 3、转成对象
        CartItem cartItem = JSON.parseObject(res, CartItem.class);
        // 4、返回
        return cartItem;
    }

    @Override
    public Cart getCart() throws ExecutionException, InterruptedException {
        /**
         * 需求分析1
         *   1、如果用户登录后，那么临时购物车的数据如何处理？
         *      将临时购物车的数据，放到用户购物车中进行合并
         *   2、如何显示购物车中的数据？
         *      从redis中查询到数据放到对象中，返回到页面进行渲染
         */
        Cart cart = new Cart();
        // 1、购物车的获取操作，分为登录后购物车 和 没登录购物车
        UserInfo userInfo = CartIntercept.threadLocal.get(); // 拿到共享数据
        if (userInfo.getUserId() != null) { // 登录后的购物车
            String cartKey = CART_PREFIX + userInfo.getUserId();
            String tempCartKey = CART_PREFIX + userInfo.getUserKey();
            // 如果临时购物车中还有数据，那就需要将临时购物车合并到已登录购物车里面
            // 判断临时购物车中是否有数据
            List<CartItem> cartItems = getCartItems(tempCartKey);
            if (cartItems != null && cartItems.size() > 0) { // 临时购物车中有数据，需要将临时购物车的数据合并到登录后的购物车
                for (CartItem item : cartItems) { // 拿到临时购物车的所有数据，将他添加到已登录购物车里面来
                    // 调用addToCart()这个方法，他会根据登录状态进行添加，当前是登录状态
                    // 所以这个方法会将临时购物车的数据添加到已登录购物车中
                    addToCart(item.getSkuId(),item.getCount()); // 合并临时和登录后的购物车
                }
                // 合并完成后还需要将临时购物车中的数据删除
                clearCart(tempCartKey);
            }
            // 再次获取用户登录后的购物车【包含临时购物车数据和已登录购物车数据】
            List<CartItem> cartItemList = getCartItems(cartKey);
            cart.setItems(cartItemList);// 将多个购物项设置到购物车中
        } else {
            // 没有登录的购物车，拿到没有登录购物车的数据
            String cartKey = CART_PREFIX + userInfo.getUserKey();
            // 获取购物车中的所有购物项
            List<CartItem> cartItems = getCartItems(cartKey);
            cart.setItems(cartItems);
        }
        return cart;
    }

    @Override
    public void checkItem(Long skuId, Integer checked) {
        // 1、从redis中查出购物项设置购物项是否选中。
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        String cart = (String) cartOps.get(skuId.toString());
        CartItem cartItem = JSON.parseObject(cart, CartItem.class);
        cartItem.setCheck(checked == 1 ? true : false);
        String jsonStirng = JSON.toJSONString(cartItem);
        cartOps.put(skuId.toString(),jsonStirng); // 更新redis
    }

    @Override
    public void updateItem(Long skuId, Integer count) {
        // 1、用户在页面对某个购物项增加或减少购物项的数量，那么redis中应该也要进行更新
        CartItem cartItem = getCartItem(skuId);
        cartItem.setCount(count);
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        cartOps.put(skuId.toString(),JSON.toJSONString(cartItem)); // 更新redis中数据
    }

    @Override
    public void deleteItem(Long skuId) {
        // 获取到当前购物车，然后根据id删除购物车里面的购物项
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        cartOps.delete(skuId.toString());
    }

    @Override
    public List<CartItem> getCurrentUserCartItem() {
        // 获取购物车中被选中购物项信息
        UserInfo userInfo = CartIntercept.threadLocal.get();
        if (userInfo.getUserId() != null) {
            String cartKey = CART_PREFIX + userInfo.getUserId();
            // 获取所有被选中的购物项
            List<CartItem> cartItems = getCartItems(cartKey);
            if (cartItems != null) {
                // 遍历当前购物车中所有的购物项，同时过滤掉没有选中的购物项
                List<CartItem> collect = cartItems.stream().filter(item -> item.getCheck()).map(item -> {
                    // 查询出当前商品最新的价格
                    BigDecimal price = productFeignService.getPrice(item.getSkuId());
                    item.setPrice(price);
                    return item;
                }).collect(Collectors.toList());
                return collect;
            }
        }
        return null;
    }


    private void clearCart(String tempCartKey) {
        redisTemplate.delete(tempCartKey);
    }

    /**
     * 获取指定用户 (登录用户/临时用户) 购物车里面的数据
     * @param cartKey
     * @return
     */
    private List<CartItem> getCartItems(String cartKey) {
        BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
        List<Object> values = operations.values(); // 拿到多个购物项
        if (values != null) {
            List<CartItem> cartItemList = values.stream().map((obj) -> {
                String s = (String) obj;
                CartItem cartItem = JSON.parseObject(s, CartItem.class);
                return cartItem;
            }).collect(Collectors.toList());
            return cartItemList;
        }
        return null;
    }

    /**
     * 获取到我们要操作的购物车
     * 返回购物车的操作方法
     * 以后想要调用购物车可以直接调用这个方法
     * @return
     */
    private BoundHashOperations<String, Object, Object> getCartOps() {
        // 要想知道用户登录了还是没有登录就进行添加购物车，请求发送给controller的时候，拦截器就开始执行了，拦截器中会拿到用户的信息
        // 在这里得到用户登录的信息，只要是同一次请求，在任何位置都能拿到
        UserInfo userInfo = CartIntercept.threadLocal.get();
        String cartKey = "";
        // 如果userInfo已经登录了，肯定是给他登录后的购物车添加商品，只有没登录，那么所有的操作才在临时购物车里面
        if (userInfo.getUserId() != null) { // 登录用户按照userid
            // gulimall:cart:1
            cartKey = CART_PREFIX + userInfo.getUserId();
        } else { // 临时用户按照userkey进行绑定
            cartKey = CART_PREFIX + userInfo.getUserKey();
        }
        /**
         * 将一个商品，添加到对应的购物车
         * 1、添加到购物车，其实有两个操作
         *      1、购物车里面之前就有了这个商品，相当于redis里面已经有了这个商品，就只需要把redis里面的商品的数量进行更改
         *      2、购物车里面没有这个商品，那么就需要给redis里面新增这个商品的信息
         */
        // 绑定了hash操作，以后所有对redis的增删改查，都是针对这个key的增删改查，也就是针对这个购物车进行操作
        BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
        return operations;
    }
}